package com.mlh.mlhspringmvc.servlet;

import com.mlh.mlhspringmvc.annotation.Controller;
import com.mlh.mlhspringmvc.annotation.RequestMapping;
import com.mlh.mlhspringmvc.annotation.RequestParam;
import com.mlh.mlhspringmvc.annotation.ResponseBody;
import com.mlh.mlhspringmvc.context.MlhWebApplicationContext;
import com.mlh.mlhspringmvc.controller.MlhHandler;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class MlhDispatcherServlet extends HttpServlet {
    MlhWebApplicationContext mlhWebApplicationContext = null;
    //定义属性handlerList，保存HspHandler[url所请求控制器方法的映射]
    List<MlhHandler> handlerList = new ArrayList<>();

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        //获取到web.xml中的contextConfigLocation
        /*
        <init-param>
        <param-name>contex tConfigLocation</ param-name>
        <param-value>classpath:hspspringmvc . xml</ param-value>
        </init-param>
        */
        String configLocation = servletConfig.getInitParameter("contextConfigLocation");
        mlhWebApplicationContext = new MlhWebApplicationContext(configLocation);
        mlhWebApplicationContext.init();
        //调用initHandlerMapping，完成url和控制器方法的映射
        initHandlerMapping();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        executeDispatcher(req, resp);
    }

    //编写方法，完成url和控制器方法的映射
    private void initHandlerMapping() {
        //判断当前的ioc容器是否为null
        if (mlhWebApplicationContext.ioc.isEmpty()) {
            return;
        }
        //遍历ioc容器的bean对象,然后进行url映射处理
        //java基础map的遍历
        for (Map.Entry<String, Object> entry : mlhWebApplicationContext.ioc.entrySet()) {
            //先取出注入的Object的clazz对象
            Class<?> clazz = entry.getValue().getClass();
            //如果注入的Bean是Controller
            if (clazz.isAnnotationPresent(Controller.class)) {
                //取出它的所有方法
                for (Method method : clazz.getDeclaredMethods()) {
                    //遍历方法
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        //判断该方法是否有@RequestMapping
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        //取出@RequestMapping的值->就是映射路径
                        String url = requestMapping.value();
                        //创建MlhHandler对象
                        handlerList.add(new MlhHandler(url, entry.getValue(), method));
                    }
                }
            }
        }
    }


    //获得对应url的handler
    private MlhHandler getMlhHandler(HttpServletRequest request) {
        //获取用户请求的uri  uri=工程路径/aa/bb
        String uri = request.getRequestURI();
        //为此我们在处理handler映射时对url都额外的加入了工程路径  这种行不通 getServletContext() 会出现莫名其妙的问题
        //还有一种解决方案就是把工程路径置为只有一个 /
        for (MlhHandler mlhHandler : handlerList) {
            if (mlhHandler.getUrl().equals(uri)) {
                return mlhHandler;
            }
        }
        return null;
    }

    //执行handler的调度分发
    private void executeDispatcher(HttpServletRequest request, HttpServletResponse response) {
        //处理提交的数据中文乱码
        try {
            request.setCharacterEncoding("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        MlhHandler handler = getMlhHandler(request);
        try {
            if (handler != null) {
                //将HttpServletRequest和HttpServletResponse封装到参数数组
                //1、得到目标方法的所有参数信息[参数数组]
                Class<?>[] parameterTypes = handler.getMethod().getParameterTypes();
                //2、创建一个数组[对应实参数组]，以便于后面反射调用目标方法的使用
                Object[] params = new Object[parameterTypes.length];
                //3、遍历parameterTypes数组，根据形参数组信息，将实参填充到实参数组
                for (int i = 0; i < parameterTypes.length; i++) {
                    Class<?> parameterType = parameterTypes[i];
                    //如果这个形参是HttpServletRequest,将request填充到params
                    if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
                        params[i] = request;
                    } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
                        params[i] = response;
                    }
                }
                //1.获取http 清求的参数集合
                //http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
                //2.返回的Map<String, String[]> String: 表示http请求的参数名
                // String[]: 表示http清求的参数值,为什么是数组
                //你看上面那个url hobby是不是有两个值  因此用String[]数组
                Map<String, String[]> parameterMap = request.getParameterMap();
                //遍历parameterMap 将请求参数，按照顺序填充到实参数组
                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                    //取key,这paramName 就是对应请求的参数名
                    String paramName = entry.getKey();
                    //说明:这里只考虑提交的参数是单值的情况，即不考虑类i以checkbox提示的数据
                    //这里做了简化， 如果小伙伴考虑多值情况，也不难..
                    String paramValue = entry.getValue()[0];
                    //得到请求的参数对应目标方法的第几个形参，然后将其填充.这里请求的参数代表的是有@requestParam注解的参数！！
                    //这里专门编写一个方法，得到请求的参数对应的是第几个形参
                    int index = getIndexRequestParameterIndex(handler.getMethod(), paramName);
                    if (index != -1) {//如果得到下标这填入实参数组
                        params[index] = paramValue;
                    } else {//如果没得到下标 则是普通参数 用另一种方法进行填充
                        //1.得到目标方法的所有形参的名称，专门编写方法获取形参名
                        //2.对得到目标方法的所有形参名进行遍历,如果匹配就把当前请求的参数值，填充到params
                        List<String> parameterNames = getParameterNames(handler.getMethod());//这个应该放在for循环外面 否则每次都要去查询一次所有方法的参数
                        for (int i = 0; i < parameterNames.size(); i++) {
                            if (paramName.equals(parameterNames.get(i))) {
                                params[i] = paramValue;
                            }
                        }
                    }
                }
                //通过反射调用controller方法
                Object result = handler.getMethod().invoke(handler.getController(), params);
                //接下来对返回的结果进行解析=>原生的springmvc可以通过视图解析器来完成
                //这里为了简化 就在分发器里面完成了
                if (result instanceof String) {
                    String viewName = (String) result;
                    if (viewName.contains(":")) {//说明返回的String结果是进行重定向或者请求转发
                        String viewType = viewName.split(":")[0];
                        String viewPage = viewName.split(":")[1];
                        //判断是forward 还是redirect
                        if ("forward".equals(viewType)) {
                            //由于这里的工程路径是/  getRequestDispatcher(viewPage)可以直接写viewPage
                            //因此 在正常写的时候还需要写工程路径名
                            request.getRequestDispatcher(viewPage).forward(request, response);
                        } else if ("redirect".equals(viewType)) {
                            response.sendRedirect(viewPage);
                        }
                    } else {//默认是请求转发  默认的情况指的是没有写forward 或者 redirect
                        //注意这里填入的是viewName
                        request.getRequestDispatcher(viewName).forward(request, response);
                    }
                } else if (result instanceof ArrayList) {//如果result 是 ArrayList
                    //查看目标方法上是否有@ResponseBody
                    if (handler.getMethod().isAnnotationPresent(ResponseBody.class)) {
                        //！！！这里引入ObjectMapper对象会出现类找不到的错误 没能力解决  因此只能用注释展示了
                        // //request 转成json格式数据
                        // //这里我们需要使用到java中如何将ArrayList转成json
                        // //这里我们需要使用jackson包下的工具类可以轻松的搞定。
                        // ObjectMapper objectMapper = new ObjectMapper();
                        // String resultJson = objectMapper.writeValueAsString(result);
                        // response.setContentType("text/html;charset=utf-8");
                        // PrintWriter writer = response.getWriter();
                        // writer.write(resultJson);
                        // writer.flush();
                        // writer.close();
                    }
                }
            } else {
                //如果没有对应的handler 则返回404
                PrintWriter writer = response.getWriter();
                writer.write("<h1>404 NOT FOUND</h1>");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    //获取到有@RequestParam注解参数的下标
    public int getIndexRequestParameterIndex(Method method,String paramName){
        //得到method的所有形参
        Parameter[] parameters = method.getParameters();
        //依次遍历这些参数
        for(int i=0;i<parameters.length;i++){
            Parameter parameter = parameters[i];
            if(parameter.isAnnotationPresent(RequestParam.class)){//看看是否有@RequestParam注解
                //有的话则取出value的值 和 实参名进行匹配
                RequestParam annotation = parameter.getAnnotation(RequestParam.class);
                String value = annotation.value();
                if(paramName.equals(value)){
                    return i;
                }
            }
        }
        return -1;
    }

    //编写方法，得到目标方法的所有形参的名称,并放入到集合中返回
    public List<String> getParameterNames(Method method){
        List<String> parameterList=new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            //这里需要引入一个plugin
            //否则parameter.getName()获取到的是arg0 arg1 arg2 ...
            //而不是真正的实参名称
            //需要使用到 jdk8 的新特性，并需要在 pom.xml 配置 maven 编译插件(可以百度搜索到)
            parameterList.add(parameter.getName());
        }
        return parameterList;
    }

}
